﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LinqKit;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections;
using System.Data.Objects;
using Linq.Autocompiler.EF4;

namespace Nexterday.Linq.Autocompiler.EF4
{
    public class ParametrizingExpressionVisitor<TContext> : LinqKit.ExpressionVisitor where TContext:ObjectContext
    {
        ParameterExpression _dataContextParam, _parameterContainerParam;
        ObjectContext _dataContext;
        List<object> _parameterValues;
        Dictionary<string, ParameterExpression> _parameterRenamingDict;
        CompilationParameters _compilationParameters;

        public static ParametrizationResult<TContext> Process<TContext>(Expression expr, CompilationParameters compilationParameters) where TContext:ObjectContext
        {
            var visitor = new ParametrizingExpressionVisitor<TContext>(compilationParameters);
            var newExpression = visitor.Visit(expr);
            var result = new ParametrizationResult<TContext>()
            {
                Expression = newExpression,
                DataContext = (TContext)visitor._dataContext,
                DataContextParameter = visitor._dataContextParam,
                ParameterContainerParameter = visitor._parameterContainerParam,
                ParameterValues = visitor._parameterValues
            };
            return result;
        }

        public static ParametrizationResult<ObjectContext> Process(Expression expr, CompilationParameters compilationParameters)
        {
            return Process<ObjectContext>(expr, compilationParameters);
        }


        private ParametrizingExpressionVisitor(CompilationParameters compilationParameters)
        {
            _dataContextParam = null;//
            _parameterContainerParam = Expression.Parameter(typeof(ParameterValueContainer), "__params");
            _parameterValues = new List<object>();
            _parameterRenamingDict = new Dictionary<string, ParameterExpression>();
            _compilationParameters = compilationParameters;
        }





        protected override Expression VisitConstant(ConstantExpression c)
        {
            if (c.Type.IsGenericType && c.Type.GetGenericTypeDefinition() == typeof(ObjectSet<>))
            {
                Type tableType = c.Type.GetGenericArguments()[0];
                var context = c.Value.GetType().GetProperty("Context").GetValue(c.Value, null);
                if (_dataContextParam == null)
                {
                    _dataContext = context as ObjectContext;
                    _dataContextParam = Expression.Parameter(typeof(TContext), "__context");
                }
                var propInfo = context.GetType().GetProperties().Where(x => x.PropertyType.Equals(c.Type)).FirstOrDefault();
                return Expression.Property(_dataContextParam, propInfo);
                
                //return Expression.Call(_dataContextParam,
                //    "CreateObjectSet", 
                //    new Type[] { tableType },
                //    new Expression[]{});
            
                
            }
            else if (c.Type.IsPrimitive && _compilationParameters.ParametrizePrimitiveConstants)
            {
                return AddParameterWithValue(c.Type, c.Value);
            }
            return base.VisitConstant(c);
        }

        protected override Expression VisitParameter(ParameterExpression p)
        {
            if (_compilationParameters.NormalizeParameterNames)
            {
                if (_parameterRenamingDict.ContainsKey(p.Name))
                {
                    return _parameterRenamingDict[p.Name];
                }
                else
                {
                    var name = "p" + _parameterRenamingDict.Count;
                    var param = Expression.Parameter(p.Type, name);
                    _parameterRenamingDict[p.Name] = param;
                    return param;
                }
            }
            return base.VisitParameter(p);
        }

        protected override Expression VisitLambda(LambdaExpression lambda)
        {
            if (_compilationParameters.NormalizeParameterNames)
            {
                return Expression.Lambda(lambda.Type, Visit(lambda.Body), lambda.Parameters.Select(x => Visit(x)).OfType<ParameterExpression>());
            }
            return base.VisitLambda(lambda);
        }

        private Expression AddParameterWithValue(Type valueType,object value)
        {
            string propName = "Value" + _parameterValues.Count;
            //Add the value to the list that is returned after visiting hours
            _parameterValues.Add(value);
            //Return a replacement expression, which retreives the value from the array and converts (casts) it to its original type
            return Expression.Convert(Expression.Property(_parameterContainerParam, propName), valueType);
        }

        protected override Expression VisitMemberAccess(MemberExpression m)
        {
            //If the member is a DisplayClass
            if (m.IsCapturedField())
            {
                FieldInfo field = m.Member as FieldInfo;
                if (field != null)
                {
                    //Get the value
                    object obj = ((ConstantExpression)m.Expression).Value;
                    var value = field.GetValue(obj);

                    Type arrayType;
                    //Expression arrayIndex;
                    //If SupportEnumerableParameters and the type is an enumerable
                    if (_compilationParameters.SupportEnumerableParameters && (arrayType = field.FieldType.GetGenericIEnumerables().FirstOrDefault()) != null)
                    {
                        var values = new List<Expression>();
                        foreach (var item in ((IEnumerable)value))
                        {
                            values.Add(AddParameterWithValue(arrayType, item));    
                        }
                        return Expression.NewArrayInit(arrayType, values);
                    }
                    else
                    {
                        return AddParameterWithValue(field.FieldType, value);
                    }
                }
            }
            return base.VisitMemberAccess(m);
        }
    }
}
